使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式使代码编制真正工程化,是软件工程的基石脉络,如同大厦的结构一样。
文章结构:
1.单一职责原则(SRP);
2.里氏替换原则(LSP);
3.依赖倒置原则(DIP);
4.接口隔离原则(ISP);
5.迪米特原则(LoD);
6.开闭原则(OCP)。
7.以计算器设计为例子综合展示各大原则以及相关设计模式。
一、单一职责原则(Single Responsibility Principle,SRP原则):
定义:
应该有且仅有一个原因引起类的变更。也就是一个接口或类只负责一件事。
使用原因:
假设一个类负责两大职责,那么当业务变化,一个职责需要变化时就要修改这个类,所以就可能导致另一个职责会发生变化。
优点:
1.类的复杂度降低,实现什么职责都又清晰明确的定义;
2.可读性高,复杂性降低;
3.可维护性高,易维护;
4.变更引起的风险降低。(一个接口修改只对应的实现类有影响,对其他的接口无影响,对系统扩展性、维护性有多大帮助)。
例子:
//这样设计的话,有个后果可以看见:如果我要让帅哥类拥有一个跳舞的技能,那么我要修改哪里呢??修改People类?那就全崩了。所以我们应该把帅哥类拆分出来成为另一个类来继承People。
public class People{
public void say(String name){
System.out.println("我是"+name);
}
}
//场景类
public class Client{
public static void main(String []args){
People people=new People();
people.say("帅哥");
people.say("美女");
people.say("大叔");
}
}
单一职责原则使用建议:
接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。
但是呢,类的设计也要结合实际,很多时候非单一职责的情况的,我们就要去妥协一下子:
1.只有逻辑足够简单,才可以在代码级别上违背SRP;
2.只有类中方法数量足够少,才可以在方法级别上违背SRP;
3.实际应用中的类都要复杂的多,一旦发生职责扩散而需要修改类时,除非这个类本身非常简单,否则还是要遵循SRP。
二、里氏变换原则(Liskov Substitution Principle,LSP原则):
定义:
所有引用基类的地方必须能透明地使用其子类对象。即:只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或寻常。(子类可完整实现父类的业务)。
设计原因:
有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
这个定义有四层含义:
### (一)、子类必须完全实现父类的方法:
也就是在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明违背LSP原则。(在编写类的实现时用父类对象,调用时传入其子类实现向上转型即可完成设计。是传入子类对象,在方法实现向上转型成父类)。
针对这一含义,实际的开发有些注意点:
1.业务增多时,类的设计思路:
(1)(不推荐)在moudle类中增加instanceof的判断(即每增加所有与这个类的父类有关系的类都要修改)。
(2)(推荐,维护人员经常干)新增类脱离继承,简历一个独立的父类,可与原来的那个父类建立关联委托关系。
2.具体应用场景类的设计关键:
子类是否能完整地实现父类的业务。(如果子类不能完整实现父类的方法,或者父类的某些方法在子类中已经发生变化,则应该简历断开父子继承关系,采用依赖、聚集、组合等关系。)
(二)子类可以有自己的个性:
也就是子类可以有自己的方法和属性。
注意:在调用那个类时,不要使用父类对象传递。因为向下转型是不安全的。
(三)覆盖或实现父类的方法时输入参数可以被放大。
相关概念:
(1)方法重载(Overload)子类拥有父类的所有属性和方法,方法名相同,但输入参数又不相同。要求方法的输入参数类或数量不相同,在LSP原则下,就是子类的输入参数宽于或等于父类的输入参数。
(2)方法覆写:父类和子类的同名方法的输入参数是相同的,而且两个方法的范围值为子类的小于父类的。
注意:
1.子类中方法的前置条件(传递的参数)必须与父类中被覆写的方法的前置条件相同或者更宽松。
2.如果父类的输入参数类型宽于子类的输入参数类型,那么会出现父类存在的地方,子类就未必可存在。因为一旦把子类作为参数传入,调用者可能进入子类的方法。
(四)、覆写或实现父类的方法时输出结果可以被缩小:
含义:
父类的一方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么LSP原则要求S必须小于或等于T。也就是说S和T是同一类型或者S是T的子类。
LSP原则的目的:
增强程序的健壮性,使版本升级时,即使增加子类,原有的子类还可以继续运行。
实际项目做法:项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑。可是!!使用LSP原则的时候要尽量避免子类的“个性”,因为LSP原则只可以正着用不能反着用。
例子:
public class Father {
public Collection doSomeing(HashMap map){
System.out.println("父类被执行。。。");
return map.values();
}
}
public class Son extends Father{
public Collection doSomeing(Map map){
System.out.println("到子类啦");
return map.values();
}
}
//场景类一。
public class Client{
public static void invoker(){
Father f=new Father(); //这里就是直接使用父类的对象
HashMap map=new HashMap();
f.doSomeing(map);
}
public static void main(String []args){
invoker();
}
}
//跟场景类一结果一样的场景二:
//分析:为啥会一样结果呢??因为我们设计子类的时候,输入的参数的范围相对于父类扩大了,子类就可以代替父类传递到父类的调用者中,子类的方法就不会被执行啦。但是呢,如果想让子类的方法被执行,就必须覆写父类的方法了。
public class Client{
public static void invoker(){
Son son=new Son();
HashMap map=new HashMap();
son.doSomeing(map);
}
public static void main(String []args){
invoker();
}
}
三、依赖倒置原则(Dependence Inversion Principle,DIP原则):
定义:简而言之,就是面向接口编程。
(1)模块间的依赖通过抽象发生,实现类之间不发生之间的依赖关系,其依赖关系是通过接口或抽象类产生的;
(2)接口或抽象类不依赖于实现类;
(3)实现类依赖接口或抽象类。
设计原因:
类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
DIP原则优点:
减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。
相关概念:
java中一个变量有两种类型:表面类型和实际类型。表面类型是定义的时候赋予的类型,实际类型是对象的类型。
使用:
分析!!分析两个类是否有依赖关系,有则制定出两者之间的接口(或抽象类)即可独立开发。
依赖的三种写法 :
(一)、在类中通过构造函数声明依赖对象。(按照依赖注入的说法,叫做构造函数注入)。
public interface IDriver{
public void drive(); //是司机就会开车.这是个司机接口
}
public class Driver implements IDriver{
private ICar car;
public Driver (ICar _car){ //构造函数注入依赖
this.car=_car;
}
//司机主要职责就是开车
public void drive(){
this.car.run(); //对象事务一级级地传递,传递到车对象
}
}
(二)在抽象中设置Setter方法声明依赖关系(依照依赖注入的说法,这叫Setter依赖注入)。
public interface IDriver{
public interface IDriver{
public void setCar(ICar car); //车辆型号
public void drive();
}
public class Driver implements IDriver{
private ICar car;
public void setCar(ICar car){ //setter函数注入依赖
this.car=car;
}
public void driver(){
this.car.run();
}
}
}
(三)接口声明依赖对象
//分析:在IDriver中,通过传入ICar接口实现**抽象之间**的依赖关系。Driver实现类也传入了ICar接口,而是什么类型Car就需要在高层声明了。
public interface ICar {
//是汽车就会跑
public void run();
}
public interface IDriver{
//是司机就会开车
public void drive(ICar car); //在接口就注入依赖,注入的是接口之间的依赖
}
publi class Driver implements IDriver{
//司机主要职责就是开车
public void drive(ICar car){
car.run();
}
}
使用DIP原则的规则以及注意点:
(一)、每个类尽量都又接口或抽象类,或者两者都具备。
(依赖倒置的基本要求,有了抽象才可能依赖倒置)
(二)变量的表面类型尽量是接口或者抽象类。
(但不是所有类型都要抽象,比如一个工具类一般不需接口或抽象类)
(三)任何类都不应该从具体类派生。
(比如:项目处于开发状态,不应从具体类派生出子类。但有维护的口占情况,这时则可以不考虑,但要求不超过两层继承)。
(四)尽量不要覆写基类的方法
(如果基类是一个抽象类,而且方法已经实现,子类应尽量不要覆写。类间依赖的是抽象,覆写抽象方法对依赖的稳定性有影响。)
(五)结合里氏替换原则使用
(原则:接口负责定义public属性和方法,并且声明与其他对象的的依赖关系,抽象类负责公共构造部分的实现,是实现类准确地实现业务逻辑,同时在适当的时候对父类进行细化)。
DIP原则是6个设计原则中最难实现的原则,是实现开闭原则的重要途径。
四、接口隔离原则(Interface Segregation Principle,ISP原则):
定义:
客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上。(一句话:接口尽量细化,同时接口中的方法尽量少。)
相关定义:接口分类:
(一)实例接口。在Java中声明一个类,然后new产生一个实例,它是对一个类型的事物的描述(一个角度:Java中的类也是一种接口)。
(二)类接口。Java中经常使用的interface关键字定义的接口。
做法:
把大接口分解为几个小接口。(例子:美女的接口分解为外貌美女、气质型美女)
注意:
单一职责要求的是类和接口职责单一,注重的是职责!!!这属于业务逻辑的划分。
接口隔离原则是对接口进行规范约束:(四层含义)开发注意点
(1)接口要尽量小。
根据接口隔离原则拆分接口时,首先必须满足单一职责原则!
(2)接口要高内聚。
高内聚:提高接口、类、模块的处理能力,减少对外的交互。要求接口–尽量少公布public方法,接口是对外的承诺,承诺越少对系统的开发越有利。
(3)定制服务:
原因:模块之间必然会有耦合,有耦合就要有相互访问的接口(可能是Java中的interface,也可能为一个类或单纯数据交换)设计时需要为各个访问者(客户端)定制服务。
定义:就是单独为一个getItem提供优良的服务
要求做法:只提供访问者需要的方法。
项目开发时接口设计规则:
(一)一个接口只服务于一个子模块或业务逻辑
(二)通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口达到“满身筋骨肉“,而不是肥嘟嘟的一堆方法。
(三)已经被污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理。
(四)了解环境,不要盲从。每个项目或产品都有特定的环境因素,别看到大师做接口就做接口。环境不同,接口拆分的标准不同,有一点点的妥协原则的。
例子:
//外貌型美女
public interface GoodBodyGirl{
//美丽的外貌
public void goodLooking{
}
//好身材
public void niceFigure{
}
}
//气质型美女
public interface GreatTemperametGirl{
//气质
public void greatTemperament();
}
(五)迪米特原则(Law Of Demeter,LOD原则):
定义:
一个对象应该只对其他对象有最少的了解。(也就是说你的类多复杂都不关我的事,我只关心你的public事物)
设计原因:
类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
含义有四层:
(一)只和朋友交流:类与类的联系不要跨越。
(也就是我想叫我的朋友的朋友去干活,就必须通过我的朋友去告诉我的朋友的朋友)
类与类之间的关系是建立在类间的,而不是方法之间,因此一个方法尽量不引入一个类中不存在的对象。
(二)朋友之间也是有距离的:
一个雷公开的public属性或方法越多,修改时设计的面也就越大,变更引起的风险扩散也越大。
注意:尽量不公布太多public方法和非静态的public变量,多使用private–>类的高内聚。
技巧:一个类暴露过多方法,而发现这堆方法有关系时,可以设计一个public方法,把那堆方法统率在新的public方法,并设置为private。
(三)是自己的就是自己的:
方法设计原则:如果一个方法放在本类中,即不增加类间的关系,也对本类不产生负面影响,就可以放置在本类中。
(四)谨慎使用Serializable(序列化机制,具体看我另一篇博客输入输出机制)。
例子:一个项目使用RMI(远程方法调用)方式传递VO值对象,其实就是个网络请求过程,那么这个对象就必须实现Serializable接口。(也就是把需要网络传输的对象进行序列化,否则会出现Not Serializable Exception)。
核心观念:类间解耦,实现弱耦合。
可能导致的后果:然而其结果会产生大量中转类或跳转类,导致系统的复杂性提高,为维护带来难度。所以使用LOD原则时要反复权衡。跳转不超过两次。
例子:
//场景是:老师叫班长清点女生人数,班长就去清点了。
//分析:所以这个场景,根据LOD原则,老师是不能调用女生的类的。然后我们知道只能够让班长去调用女生。而老师去调用班长。
public class Teacher{
public void commond(GroupLeader groupleader){
groupleader.countGirls();
}
}
public class Girl{
}
public class GroupLeader{
private List<Girl> listGirls;
//传递全班的女生进来
public GroupLeader(List<Girl> _listGirls){
this.listGirls = _listGirls;
}
//清查女生数量
public void countGirls(){
System.out.println("女生数量是:"+this.listGirls.size());
}
}
六、开闭原则:
定义:
一个软件实体应通过扩展来实现变化,而不是通过修改已有的代码来实现变化。
设计原因:
在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。
相关概念:
软件实体:
(1)项目或软件产品中按照一定逻辑规则划分的模块。
(2)抽象和类。
(3)方法。
业务变化:(三种类型)
(1)逻辑变化。
只变化一个逻辑而不涉及其他模块,可通过修改原有类中的方法的方式来完成。但是有个前提条件:所有依赖或关联类都按照相同逻辑处理。
(2)子模块变化:
一个模块变化,必会对其他模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化。
(3)可见视图变化:
可见视图是提供给客户使用的界面,该部分的变化一般会引起连锁反应。因为多了一项显示数据,意味着你的其他部分代码也要增多一项这样的数据。
针对软件产品的需求业务变化:有三大扩展方案
(一)修改接口,增加方法:(否定)
后果:所有拥有该接口的实现类必须作修改,场景类也要修改。
否定这个方案的原因:接口应该是稳定且可靠的,不应经常发生变化,否则接口作为契约的作用就失去了。
(二)修改实现类:(不推荐)
做法:通过class文件替换的方式完成业务变化。
后果:修改了的信息可能会导致其他类获得的信息不对称。
(三)通过扩展实现变化:(推荐)
做法:增加子类,覆写方法,高层次的模块(static静态模块区)通过新增子类产生新的对象。
代码例子:
public interface Book{
//书籍有名称
public String getName();
//书籍有售价
public int getPrice();
//书籍有作者
public String getAuthor();
}
public class NovelBook implements Book{
//书籍名称
private String name;
//书籍的价格
private int price;
//书籍的作者
private String author;
//通过构造函数传递书籍数据
public NovelBook(String _name,int _price,String _author){
this.name = _name;
this.price = _price;
this.author = author;
}
public String getAuthor(){
return this.author;
}
public String getName(){
return this.name;
}
public int getPrice(){
return this.price;
}
}
//然后到了阅读月啦,我们书店就要打折啦!!怎么办??正如我们刚刚提到的三种方案。
//分析:如果选择第一种方案,在接口里面新增方法,那么我们的小说类NovelBook也要修改,而且违背接口的稳定性原则。如果选择第二种方案,直接修改实现类,也就是修改NovelBook的类啦,是的,我们可以做一个打折的方法,然后完成打折的业务扩展,但是!!其他业务就有可能有影响,比如设计模式里面说的采购人员也看价格,而当这个方法实现了,采购人员只会看到打折后的价格。
//所以我们采用第三种方案。来完成业务扩展。
public class OffNovelBook extends NovelBook{
public offNovelBook(String _name,int _price,String _author){
super(_name,_price,_author);
}
@Override
public int getPrice(){
//原价
int selfPrice = super.getPrice();
int offPrice = 0;
if(selfPrice>4000){
offPrice = selfPrice * 9/10;
}else{
offPrice = self * 8/10;
}
return offPrice;
}
}
开闭原则是最基础的一个原则,也就是说前五个原则是指导设计的工具和方法,而开闭原则才是精神领袖。可以说,只要遵守前五大原则,设计出来的软件就会符合开闭原则。
思想核心:用抽象构建框架,用实现扩展细节。
联系其他原则:单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。
七、计算器(结合了六大设计原则与工厂模式)
//这个就是运算类的工厂模式。因为运算太多了,如果每个都在运算类里面再写个几行,那样会导致运算类的繁重累赘而且不清晰。以多态特性来设计。
public class OperationFactory{
public static Operation createOperate(String operate){
Operation oper = null;
swich(operate){
case "+":
oper = new OperationAdd();
break;
case "-":
oper = new OperationSub();
break;
case "*":
oper = new OperationMul();
break;
case "/":
oper = new OperationDiv();
break;
}
return oper;
}
}
//而这个运算类,把他给抽象处理就体现出 用抽象构建架构,用实现扩展细节了。
//分析:我们把运算类的核心抽取出来:两个数,一个结果,还需一个运算规则。这几个就是本质!而运算规则有很多种,我们需要交给子类去实现
//根据 单一职责原则--我们把每个运算规则的类分开成了单独的类。加减乘除均为一个类。
//根据 里氏替换原则--加减乘除的四个类均继承一个抽象出来的运算父类,获得一样的特性。两个数,一个结果。
//根据 依赖导致原则--面向接口编程,也就是面对抽象的结果编程。而我们抽象出的运算父类,拥有各个运算规则子类的共同特征。
//根据 接口隔离原则--呃,,这里只作出了一步抽象,没有多余的接口。
//根据 迪米特原则--松耦合。当然啦,都把加减乘除分离开来成一个类了,以后你想要修改加法,比如根据职位不同去加不同的薪水,这样更改,你只需改加法这个类。
//根据 开闭原则。核心--用抽象构建框架,用实现扩展细节。以上原则都实现了,这不就好了咩。
public abstract class Operation{
private double numberA = 0;
private double numberB = 0;
private double result = 0;
public void setNumberA(Double _numberA){
this.numberA = _numberA;
}
public double getNumberA(){
return numberA;
}
public void setNumberB(Double _numberB){
this.numberB = _numberB;
}
public double getNumberB(){
return numberB;
}
public abstract double getResult();
}
//加法类
class OperationAdd extends Operation {
public double getResult(){
result = numberA + numberB;
return result;
}
}
//减法类
class OperationSub extends Operation {
public double getResult(){
result = numberA - numberB;
return result;
}
}
//乘法类
class OperationMul extends Operation {
public double getResult(){
result = numberA - numberB;
return result;
}
}
//除法类
class OperationDiv extends Operation {
public double getResult(){
try{
result = numberA / numberB;
}catch(Exception e){
System.out.println("除数不能为0");
e.printStackTrace();
}
return result;
}
}
//场景类代码:
Operation oper;
oper = OperationFactory.createOperate(“+”);
oper.setNumberA(1.0);
oper.SetNumberB(2.0);
double result = oper.GetResult();
好了,设计模式-六大设计原则(附加实际开发建议以及计算器例子)讲完了。本博客借鉴了一部分设计模式之禅,这是一本很好的书,只是我在这里总结以及写出自己的理解。欢迎在下面指出错误,共同学习!
转载请注明:【JackFrost的博客】
更多内容,可以访问JackFrost的博客